﻿#region Usings

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Web;
using System.Web.Caching;
using RailNet.Dao.Entity;
using RailNet.Dao.Extensions;

#endregion

namespace RailNet.Dao.Repository
{
    public abstract class Repository<T> : IRepository<T>, ICacheableRepository<T>
        where T : class, IEntity
    {
        private readonly ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim();
        protected RailnetContext Context;

        private Cache _cache;

        protected Repository()
        {
            Context = new RailnetContext();
            Context.Configuration.AutoDetectChangesEnabled = false;
        }

        protected virtual DbSet<T> DbSet
        {
            get { return Context.Set<T>(); }
        }

        #region ICacheableRepository<T> Members

        /// <summary>
        /// Gets the cache from the current http context
        /// </summary>
        public Cache Cache
        {
            get
            {
                if (_cache != null)
                {
                    return _cache;
                }
                _cache = HttpContext.Current.Cache;
                return _cache;
            }
        }

        /// <summary>
        ///   Gets the cache list
        /// </summary>
        /// <returns></returns>
        public IDictionary<long, T> CacheGetList()
        {
            var type = typeof (T).ToString();
            var current = Cache[type] as IDictionary<long, T>;
            if (current != null)
            {
                return current;
            }

            var list = new Dictionary<long, T>();
            Cache[type] = list;
            return list;
        }

        /// <summary>
        ///   Gets instance by id from the cache
        /// </summary>
        /// <param name = "id">Id</param>
        /// <returns></returns>
        public T CacheGet(long id)
        {
            _cacheLock.EnterReadLock();
            var list = CacheGetList();
            if (list == null)
            {
                return default(T);
            }
            var instance = list.ContainsKey(id) ? list[id] : default(T);
            _cacheLock.ExitReadLock();
            return instance;
        }

        /// <summary>
        ///   Inserts or updates instance from cache
        /// </summary>
        /// <param name = "instance"></param>
        public void CacheInsertOrUpdate(T instance)
        {
            _cacheLock.EnterWriteLock();

            var list = CacheGetList();

            var id = instance.Id;
            if (id == 0)
            {
                if (list.Count > 0)
                {
                    id = (from l in list
                          orderby l.Key descending
                          select l.Key).First();
                }
                id++;
                instance.Id = id;

                if (list.ContainsKey(id))
                {
                    list[id] = instance;
                }
                else
                {
                    list.Add(id, instance);
                }
            }
            else
            {
                list[id] = instance;
            }
            _cacheLock.ExitWriteLock();
        }

        /// <summary>
        ///   Deletes instance from cache
        /// </summary>
        /// <param name = "instance"></param>
        public void CacheDelete(T instance)
        {
            _cacheLock.EnterWriteLock();

            var list = CacheGetList();
            if (list.ContainsKey(instance.Id))
            {
                list.Remove(instance.Id);
            }

            _cacheLock.ExitWriteLock();
        }

        #endregion

        #region IRepository<T> Members

        /// <summary>
        /// Disposes the context
        /// </summary>
        public void Dispose()
        {
            if (Context != null)
                Context.Dispose();
        }

        /// <summary>
        /// Gets all entities from DB set
        /// </summary>
        /// <returns></returns>
        public virtual IQueryable<T> All()
        {
            return DbSet.AsNoTracking();
        }

        /// <summary>
        /// Gets all entities from the DB set including specified properties
        /// </summary>
        /// <param name="includeProperties">Include properties expression</param>
        /// <returns></returns>
        public virtual IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties)
        {
            IQueryable<T> query = DbSet.AsNoTracking();
            return includeProperties.Aggregate(query, (current, includeProperty) => current.IncludeDeep(includeProperty));
        }

        /// <summary>
        /// Filters entities from the DB set based on the given predicate filter
        /// </summary>
        /// <param name="predicate">Predicate filter</param>
        /// <returns></returns>
        public virtual IQueryable<T> Filter(Expression<Func<T, bool>> predicate)
        {
            return DbSet.Where(predicate).AsQueryable().AsNoTracking();
        }

        /// <summary>
        /// Filters entities from the DB set based on the given predicate filter, given page size and page index
        /// </summary>
        /// <param name="filter">Predicate filter</param>
        /// <param name="total">Output parameter of total number of found entities</param>
        /// <param name="index">Page index</param>
        /// <param name="size">Page size</param>
        /// <returns></returns>
        public virtual IQueryable<T> Filter(Expression<Func<T, bool>> filter, int index = 0, int size = 50)
        {
            var skipCount = index*size;
            var resetSet = filter != null ? DbSet.Where(filter).AsQueryable() : DbSet.AsQueryable();
            resetSet = skipCount == 0 ? resetSet.Take(size) : resetSet.Skip(skipCount).Take(size);
            total = resetSet.Count();
            return resetSet.AsNoTracking().AsQueryable();
        }

        /// <summary>
        /// Finds an entity by key from the DB set
        /// </summary>
        /// <param name="keys">Keys</param>
        /// <returns></returns>
        public virtual T Find(params object[] keys)
        {
            return DbSet.Find(keys);
        }

        /// <summary>
        /// Finds an entity (first or default) by given predicate expression
        /// </summary>
        /// <param name="predicate"></param>
        /// <returns></returns>
        public virtual T Find(Expression<Func<T, bool>> predicate)
        {
            return DbSet.FirstOrDefault(predicate);
        }

        /// <summary>
        /// Creates new entity instance and adds it to the DB set
        /// </summary>
        /// <param name="instance">Instance</param>
        /// <returns></returns>
        public virtual T Create(T instance)
        {
            var newEntry = DbSet.Add(instance);
            Context.SaveChanges();
            return newEntry;
        }

        /// <summary>
        /// Updates given entity instance in the DB set
        /// </summary>
        /// <param name="instance">Instance</param>
        /// <returns></returns>
        public virtual int Update(T instance)
        {
            var entry = Context.Entry(instance);
            DbSet.Attach(instance);
            entry.State = EntityState.Modified;
            return Context.SaveChanges();
        }

        /// <summary>
        /// Deletes given entity instance from the DB set
        /// </summary>
        /// <param name="instance">Instance</param>
        /// <returns></returns>
        public virtual int Delete(T instance)
        {
            // remove from the DB
            var entry = Context.Entry(instance);
            if (entry.State == EntityState.Detached)
            {
                DbSet.Attach(instance);
            }
            DbSet.Remove(instance);
            return Context.SaveChanges();
        }

        #endregion
    }
}